﻿namespace Suture.StubGeneration
{
    using System;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    using System.Reflection;
    using System.Reflection.Emit;
    using System.Text;

    internal static class SimpleDynamicProxy
    {
        public static T CreateStub<T>()
        {
            return (T)CreateStub(typeof(T));
        }
        [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Reviewed. Suppression is OK here.")]
        public static object CreateStub(Type type)
        {
            var assName = new AssemblyName(Guid.NewGuid().ToString());
            var assBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assName, AssemblyBuilderAccess.RunAndSave);
            var moduleBuilder = assBuilder.DefineDynamicModule(Guid.NewGuid().ToString());
            var typeBuilder = moduleBuilder.DefineType(type.Name + "Stub", TypeAttributes.Public);

            typeBuilder.AddInterfaceImplementation(type);

            var ctorBuilder = typeBuilder.DefineConstructor(
                      MethodAttributes.Public,
                      CallingConventions.Standard,
                      new Type[] { });

            var ilGenerator = ctorBuilder.GetILGenerator();
            ilGenerator.Emit(OpCodes.Ret);

            CreateProperties(type, typeBuilder);
            CreateMethods(type, typeBuilder);

            var stubType = typeBuilder.CreateType();
            var instance = Activator.CreateInstance(stubType);
            return instance;
        }

        [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Reviewed. Suppression is OK here.")]
        private static void CreateMethods(Type type, TypeBuilder typeBuilder)
        {
            foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(mi => !mi.Name.StartsWith("get_") && !mi.Name.StartsWith("set_")))
            {
                var methodBuilder = typeBuilder.DefineMethod(
                    method.Name,
                    MethodAttributes.Public | MethodAttributes.Virtual,
                    method.ReturnType,
                    method.GetParameters().Select(p => p.ParameterType).ToArray());

                var ilGenerator = methodBuilder.GetILGenerator();

                if (method.ReturnType == typeof(void))
                {
                    ilGenerator.Emit(OpCodes.Ret);
                }
                else
                {
                    var fieldBuilder = typeBuilder.DefineField(
                        method.Name + "_retVal", method.ReturnType, FieldAttributes.Private);
                    ilGenerator.Emit(OpCodes.Ldarg_0);
                    ilGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
                    ilGenerator.Emit(OpCodes.Ret);
                }

                typeBuilder.DefineMethodOverride(methodBuilder, method);
            }
        }

        private static void CreateProperties(Type type, TypeBuilder typeBuilder)
        {
            foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                var propertyName = prop.Name;
                var propertyType = prop.PropertyType;
                var fieldBuilder = CreatePropertyBackingField(typeBuilder, propertyName, propertyType);

                CreateGetter(typeBuilder, prop, propertyName, propertyType, fieldBuilder);
                CreateSetter(typeBuilder, prop, propertyName, propertyType, fieldBuilder);
            }
        }

        private static FieldBuilder CreatePropertyBackingField(TypeBuilder typeBuilder, string propertyName, Type propertyType)
        {
            var fieldName = GenerateFieldName(propertyName);
            return typeBuilder.DefineField(fieldName, propertyType, FieldAttributes.Private);
        }

        private static void CreateGetter(TypeBuilder typeBuilder, PropertyInfo prop, string propertyName,  Type propertyType, FieldBuilder fieldBuilder)
        {
            var getterName = "get_" + propertyName;
            var getterMethodFromInterface = prop.GetGetMethod(true);

            var getterMethodBuilder = typeBuilder.DefineMethod(
                getterName, MethodAttributes.Private | MethodAttributes.Virtual | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);

            var getterILGenerator = getterMethodBuilder.GetILGenerator();

            getterILGenerator.Emit(OpCodes.Ldarg_0);
            getterILGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
            getterILGenerator.Emit(OpCodes.Ret);
            typeBuilder.DefineMethodOverride(getterMethodBuilder, getterMethodFromInterface);
        }

        private static void CreateSetter(
            TypeBuilder typeBuilder, PropertyInfo prop, string propertyName, Type propertyType, FieldBuilder fieldBuilder)
        {
            var setterMethodFromInterface = prop.GetSetMethod(true);

            if (setterMethodFromInterface == null)
            {
                return;
            }

            var setterName = "set_" + propertyName;
            var setterMethodBuilder = typeBuilder.DefineMethod(
                setterName, MethodAttributes.Private | MethodAttributes.Virtual | MethodAttributes.HideBySig, null, new[] { propertyType });

            var setterIlGenerator = setterMethodBuilder.GetILGenerator();

            setterIlGenerator.Emit(OpCodes.Ldarg_0);
            setterIlGenerator.Emit(OpCodes.Ldarg_1);
            setterIlGenerator.Emit(OpCodes.Stfld, fieldBuilder);
            setterIlGenerator.Emit(OpCodes.Ret);

            typeBuilder.DefineMethodOverride(setterMethodBuilder, setterMethodFromInterface);
        }

        private static string GenerateFieldName(string str)
        {
            var sb = new StringBuilder("_");
            sb.Append(char.ToLower(str[0]));
            sb.Append(str.Substring(1));
            return sb.ToString();
        }
    }
}